package org.litesoft.p2pchat;

import java.io.*;
import java.net.*;


// Copyright Status:
//
// All Software available from LiteSoft.org (including this file) is
// hereby released into the public domain.
//
// It is free!  As in, you may use it freely in both commercial and
// non-commercial applications, bundle it with your software
// distribution, include it on a CD-ROM, list the source code in a book,
// mirror the documentation at your own web site, or use it in any other
// way you see fit.
//
// NO Warranty!
//
// All software is provided "as is".
//
// There is ABSOLUTELY NO WARRANTY OF ANY KIND: not for the design, fitness
// (for a particular purpose), level of errors (or lack thereof), or
// applicability of this software.  The entire risk as to the quality
// and performance of this software is with you.  Should this software
// prove defective, you assume the cost of all necessary servicing, repair
// or correction.
//
// In no event unless required by applicable law or agreed to in writing
// will any party who created or may modify and/or redistribute this
// software, be liable to you for damages, including any general,
// special, incidental or consequential damages arising out of the use or
// inability to use this software (including but not limited to loss of
// data or data being rendered inaccurate or losses sustained by you or
// third parties or a failure of this software to operate with any
// other programs), even if such holder or other party has been advised
// of the possibility of such damages.
//
// NOTE: Should you discover a bug, have a recogmendation for a change, wish
// to submit modifications, or wish to add new classes/functionality,
// please email them to:
//
//        changes@litesoft.org
//

/**
 * @author  Devin Smith and George Smith
 * @version 0.3 02/02/02 Added IllegalArgument.ifNull for all public params that may not be null
 * @version 0.2 01/28/02 Refactored and Added Licence
 * @version 0.1 12/27/01 Initial Version
 */
public abstract class AbstractP2PChat
{
    private static final String VERSION = "0.2";

    private static final int DEFAULTPORT = 11581;

    protected static String getTitle()
    {
        return "P2PChat ver " + VERSION;
    }

    protected static void dumpHelp( String problem )
    {
        System.out.println( getTitle() );
        System.out.println();
        System.out.println( problem );
        System.out.println();
        System.out.println( "This program takes 1-n arguments:" );
        System.out.println( "   1st - Chatname" );
        System.out.println( "   nth - our Address and/or port" );
        System.out.println( "         peer(s) Address and optional port" );
        System.out.println();
        System.out.println( "   An Address is a standard dotted address (eg 192.168.1.5)" );
        System.out.println( "   A Port is a simple integer (default: " + DEFAULTPORT + ")" );
        System.out.println( "   A colon (':') seperates the address and port (eg 192.168.1.5:5432)" );
        System.out.println( "   Our Address and/or port is indictaed by being surrounded by square" );
        System.out.println( "      brackets (eg [192.168.1.5:5432])" );
        System.exit( 0 );
    }

    abstract protected UserDialog getUserDialog( MyInfo pMyInfo );

    protected void init( String[] args )
    {
        ArgsParser parser = new ArgsParser();

        String error = parser.parse( args );

        if ( error != null )
            dumpHelp( error );

        try
        {
            UserDialog userDialog = getUserDialog( parser.zMyInfo );

            PendingPeerManager ppm = new PendingPeerManager( userDialog );
            PeerInfo[] initialPeers = parser.zInitialPeers;
            if ( initialPeers != null )
                for ( int i = 0 ; i < initialPeers.length ; i++ )
                    ppm.addNewPeer( initialPeers[ i ] );

            ServerSocket serverSocket = getServerSocket( parser.zMyInfo.getPort() );

            new ActivePeerManager( parser.zMyInfo , userDialog , ppm );

            while ( true )
            {
                ppm.addNewPeer( serverSocket.accept() );
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();
            System.exit( 1 );
        }
    }

    private ServerSocket getServerSocket( int pPort )
    {
        try
        {
            return new ServerSocket( pPort );
        }
        catch ( IOException e )
        {
            dumpHelp( "Unable to Open Socket Listener on Port number(" + pPort + ").  Error: " + e.getMessage() );
        }
        return null;// Not Reachable
    }

    // An Address is a standard dotted address (eg 192.168.1.5)
    // A Port is a simple integer
    // A colon (':') seperates the address and port (eg 192.168.1.5:5432)
    // Our's is indictaed by surrounding square brackets (eg [192.168.1.5:5432])
    private static class ArgsParser
    {
        public MyInfo zMyInfo = null;
        public PeerInfo[] zInitialPeers = null;

        public String parse( String[] args )
        {
            if ( (args == null) || (args.length == 0) )
            {
                return "Not enough parameters.";
            }

            String chatName = args[ 0 ].trim();
            if ( chatName.length() == 0 )
            {
                return "No chatname specified.";
            }

            for ( int i = 1 ; i < args.length ; i++ )
            {
                String err , s = args[ i ];
                if ( s.startsWith( "[" ) && s.endsWith( "]" ) )
                    err = parseMyInfo( chatName , s.substring( 1 , s.length() - 1 ) );
                else
                    err = parsePeerInfo( s );
                if ( err != null )
                    return err;
            }
            if ( zMyInfo == null )
                zMyInfo = new MyInfo( chatName , null , DEFAULTPORT );
            return null;
        }

        private String parseMyInfo( String pChatName , String pParm )
        {
            if ( zMyInfo != null )
                return "My Info (" + zMyInfo + ") already given.";

            AddressPort ap = new AddressPort();

            String err = ap.parse( pParm );

            if ( err != null )
                return err;

            zMyInfo = new MyInfo( pChatName , ap.zAddress , ap.zPort );

            return null; // No Error
        }

        private String parsePeerInfo( String pParm )
        {
            AddressPort ap = new AddressPort();

            String err = ap.parse( pParm );

            if ( err != null )
                return err;

            if ( ap.zAddress == null )
                return "Peer Address not found in: " + pParm;

            if ( zInitialPeers == null )
                zInitialPeers = new PeerInfo[ 1 ];
            else
            {
                PeerInfo[] temp = new PeerInfo[ zInitialPeers.length + 1 ];
                System.arraycopy( zInitialPeers , 0 , temp , 0 , zInitialPeers.length );
                zInitialPeers = temp; // From   , At , To   , At , For
            }
            zInitialPeers[ zInitialPeers.length - 1 ] = new PeerInfo( null , ap.zAddress , ap.zPort );

            return null;
        }

        // An Address is a standard dotted address (eg 192.168.1.5)
        // A Port is a simple integer
        // A colon (':') seperates the address and port (eg 192.168.1.5:5432)
        // Our's is indictaed by surrounding square brackets (eg [192.168.1.5:5432])
        private static class AddressPort
        {
            public String zAddress = null;
            public int zPort = DEFAULTPORT;

            public String parse( String pParm )
            {
                IllegalArgument.ifNull( "Parm" , pParm );

                String err;
                int colonAt = (pParm = pParm.trim()).indexOf( ':' );

                if ( colonAt != -1 )
                {
                    if ( null != (err = parseAddress( pParm.substring( 0 , colonAt ) )) )
                        return "Invalid Address in (" + pParm + "): " + err;
                    if ( null != (err = parsePort( pParm.substring( colonAt + 1 ) )) )
                        return "Invalid Port in (" + pParm + "): " + err;
                    return null;
                }
                if ( isAllDigits( pParm ) )
                {
                    if ( null != (err = parsePort( pParm )) )
                        return "Port (" + pParm + ") Invalid: " + err;
                }
                else
                {
                    if ( null != (err = parseAddress( pParm )) )
                        return "Address (" + pParm + ") Invalid: " + err;
                }
                return null;
            }

            private String parsePort( String pParm )
            {
                try
                {
                    zPort = Integer.parseInt( pParm.trim() );
                }
                catch ( NumberFormatException e )
                {
                    return "Not a number.";
                }
                if ( zPort < 1024 || zPort >= (2 << 16) )
                    return "Not within range. Must be a 16 bit value greater then 1024";
                return null;
            }

            private String parseAddress( String pParm )
            {
                if ( (pParm = pParm.trim()).length() == 0 )
                {
                    zAddress = MyInfo.getIPs();
                    return null;
                }
                for ( int commaAt ; -1 != (commaAt = pParm.indexOf( ',' )) ; pParm = pParm.substring( commaAt + 1 ).trim() )
                {
                    String err = parseAnAddress( pParm.substring( 0 , commaAt ) );
                    if ( err != null )
                        return err;
                }
                return parseAnAddress( pParm );
            }

            private String parseAnAddress( String pParm )
            {
                int dotAt = pParm.lastIndexOf( '.' );

                if ( dotAt == -1 )
                    return "No dot ('.') in: " + pParm;

                String lastField = pParm.substring( dotAt + 1 );

                if ( isAllDigits( lastField ) )
                    return parseDirectAddress( pParm );

                if ( lastField.length() == 0 )
                    return "No TLD in: " + pParm;

                if ( -1 != pParm.indexOf( ' ' ) )
                    return "Illegal Domain Reference in: " + pParm;

                String[] directAddresses = convertAddressDomainToDirects( pParm );

                if ( directAddresses == null )
                    return "Unresolvable Domain Reference: " + pParm;

                for ( int i = 0 ; i < directAddresses.length ; i++ )
                    addAddress( directAddresses[ i ] );

                return null;
            }

            private void addAddress( String pAddress )
            {
                if ( zAddress == null )
                    zAddress = pAddress;
                else
                    zAddress += "," + pAddress;
            }

            private boolean isAllDigits( String pParm )
            {
                if ( pParm.length() == 0 )
                    return false;
                for ( int i = pParm.length() ; i-- > 0 ; )
                    if ( !Character.isDigit( pParm.charAt( i ) ) )
                        return false;
                return true;
            }

            private String[] convertAddressDomainToDirects( String pParm )
            {
                InetAddress[] addresses;
                try
                {
                    addresses = InetAddress.getAllByName( pParm );
                }
                catch ( UnknownHostException e )
                {
                    return null;
                }
                if ( (addresses == null) || (addresses.length == 0) )
                    return null;

                String[] retval = new String[ addresses.length ];
                for ( int i = 0 ; i < addresses.length ; i++ )
                    retval[ i ] = addresses[ i ].toString();

                return retval;
            }

            private String parseDirectAddress( String pParm )
            {
                DottedAddress da = new DottedAddress( pParm );
                for ( int i = 0 ; i < da.zParts.length ; i++ )
                    if ( !isAllDigits( da.zParts[ i ] ) )
                        return "Non-Numeric (" + da.zParts[ i ] + ") Dotted Address in: " + pParm;

                if ( da.zParts.length > 4 )
                    return "Too Many Parts in Address: " + pParm;

                if ( da.zParts.length == 4 )
                {
                    addAddress( da.toString() );
                    return null;
                }

                String s;
                for ( int i = 0 ; null != (s = ThisMachine.getIPAddress( i )) ; i++ )
                {
                    DottedAddress us = new DottedAddress( s );
                    us.overlayFrom( da );
                    addAddress( us.toString() );
                }
                return null;
            }
        }

        private static class DottedAddress
        {
            public String[] zParts = null; // Back to Front

            public DottedAddress( String pParm )
            {
                if ( pParm == null )
                    return;

                zParts = new String[ 0 ];

                while ( pParm.startsWith( "." ) )
                    pParm = pParm.substring( 1 );

                if ( pParm.length() == 0 )
                    return;

                for ( int dotAt ; -1 != (dotAt = pParm.indexOf( '.' )) ; pParm = pParm.substring( dotAt + 1 ) )
                    addPart( pParm.substring( 0 , dotAt ) );
                addPart( pParm );
            }

            private void addPart( String pPart )
            {
                if ( zParts.length == 0 )
                    zParts = new String[ 1 ];
                else
                {
                    String[] temp = new String[ zParts.length + 1 ];
                    System.arraycopy( zParts , 0 , temp , 1 , zParts.length );
                    zParts = temp; // From   , At , To   , At , For
                }
                zParts[ 0 ] = pPart;
            }

            public void overlayFrom( DottedAddress pParm )
            {
                if ( pParm != null )
                    for ( int i = 0 ; i < pParm.zParts.length ; i++ )
                        this.zParts[ i ] = pParm.zParts[ i ];
            }

            public String toString()
            {
                if ( zParts.length == 0 )
                    return "No Address";
                StringBuffer sb = new StringBuffer( 16 );
                for ( int i = zParts.length ; i-- > 0 ; )
                {
                    sb.append( '.' );
                    sb.append( zParts[ i ] );
                }
                return sb.toString().substring( 1 );
            }
        }
    }
}
